package com.tosmart.jorm.impl;

import com.tosmart.jorm.annotation.*;
import com.tosmart.jorm.util.StringUtil;

import java.lang.reflect.Field;
import java.util.*;

/**
 * Such a framework is an important realization
 * of JORM part.
 * <p/>
 * JORM the basic framework for reflection support.
 *
 * @author tosmart@gmail.com
 * @see JormReflect
 * @see com.tosmart.jorm.impl.SqlBuilder
 */
public class JormReflect extends ReflectBase {

    /**
     * Entity class to determine whether to use
     * optimistic locking.
     *
     * @param entityClass Entity class
     * @return whether to use optimistic locking.
     */
    public static boolean useOptimisticLock(Class entityClass) {
        return StringUtil.notEmpty(optimisticLock(entityClass));
    }

    /**
     * The use of reflective way, to determine whether
     * the Entity class, there are primaryKey attribute
     * specified fields
     *
     * @param field Entity field
     * @return true if primaryKey=true, false otherwise
     */
    public static boolean isPrimaryKey(Field field) {
        JormMap jormMap = field.getAnnotation(JormMap.class);
        return (jormMap != null && jormMap.primaryKey());
    }

    /**
     * To determine whether the current field to bind
     * the column is set to automatically generate a
     * mechanism.
     *
     * @param field field of Entity class
     * @return true of false
     */
    public static boolean isAutoGenerateColumn(Field field) {
        GenerateMethod method = JormReflect.getAutoGenerateMethod(field);
        return !method.equals(GenerateMethod.None);
    }

    /**
     * Method uses reflection to get the values of
     * column list of Entity instance.
     *
     * @param entity  Entity instance
     * @param columns name of columns
     * @return the values of the columns
     * @throws IllegalAccessException Possible throw by field getter
     */
    public static Object[] getColumnValues(
            Object entity, String[] columns)
            throws IllegalAccessException {

        List<Object> values = new ArrayList<Object>();

        for (String column : columns) {
            Field field = findField(
                    entity.getClass(),
                    column
            );
            field.setAccessible(true);
            values.add(field.get(entity));
        }

        return values.toArray();
    }

    /**
     * Using the reflection method, are listed in the
     * Entity annotation-based primary key field list.
     *
     * @param entityClass Entity class
     * @return list of the primary key field.
     */
    public static List<Field> getPkFields(Class entityClass) {

        List<Field> primaryKeys = new ArrayList<Field>();
        Field[] fields = entityClass.getDeclaredFields();

        for (Field field : fields) {
            JormMap jormMap = field.getAnnotation(JormMap.class);
            if (jormMap != null && jormMap.primaryKey()) {
                primaryKeys.add(field);
            }
        }

        return primaryKeys;
    }

    /**
     * Method uses reflection to get the values of
     * fields list of an Object.
     *
     * @param object class Object's instance
     * @param fields fields list you want get them values
     * @return the values list
     * @throws IllegalAccessException Possible throw by field getter.
     */
    public static Object[] getFieldValues(
            Object object, List<Field> fields)
            throws IllegalAccessException {

        List<Object> values = new ArrayList<Object>();

        for (Field field : fields) {
            field.setAccessible(true);
            values.add(field.get(object));
        }

        return values.toArray();
    }

    /**
     * Using the reflection method, filter field in the list
     * of all non-primary key field to list ways to return
     * their values.
     *
     * @param entity Entity instance
     * @param fields all fields of the Entity instance
     * @return the values list
     * @throws IllegalAccessException Possible throw by field getter.
     */
    public static List<Object> getFieldValuesExceptPk(
            Object entity, List<Field> fields)
            throws IllegalAccessException {

        List<Object> values = new ArrayList<Object>();

        for (Field field : fields) {
            if (isPrimaryKey(field)) continue;
            field.setAccessible(true);
            values.add(field.get(entity));
        }

        return values;
    }

    /**
     * Using the reflection method, filter field in the list
     * of all primary key field to list ways to return
     * their values.
     *
     * @param entity Entity instance
     * @param fields all fields of the Entity instance
     * @return the values list
     * @throws IllegalAccessException Possible throw by field getter.
     */
    public static List<Object> getPkFieldValues(
            Object entity, List<Field> fields)
            throws IllegalAccessException {

        List<Object> pkValues = new ArrayList<Object>();
        // for where clause's pk values
        for (Field field : fields) {
            if (!isPrimaryKey(field)) continue;
            field.setAccessible(true);
            pkValues.add(field.get(entity));
        }
        return pkValues;
    }

    /**
     * In this way the first method uses reflection
     * to get all the Entity instance associated with
     * the database table column names.
     * <p/>
     * Then according to the column name, read from
     * the Map corresponds to the value of this column.
     * <p/>
     * Finally, set the Entity in this list corresponds
     * to an instance of the field values.
     *
     * @param entity Entity instance
     * @param map    the Map with the values from database
     * @param <T>    Entity class
     * @throws IllegalAccessException Possible throw by field setter
     */
    protected static <T> void setFieldValues(
            T entity, Map<String, Object> map)
            throws IllegalAccessException {

        List<Field> fields = getMapFields(
                entity.getClass()
        );

        for (Field field : fields) {
            String columnName = getOrmMapColumn(field);
            Object value = map.get(columnName.toUpperCase());
            setTypedFieldValue(entity, field, value);
        }
    }

    /**
     * This method is used to set the field value Object
     * instance.
     * <p/>
     * First of all, access to the type of field, and
     * then set the value to be converted to a clear
     * field type, and then be set.
     *
     * @param object Object which need to set field value
     * @param field  the field of object
     * @param value  the value to set
     * @throws IllegalAccessException Possible throw by field setter
     */
    public static void setTypedFieldValue(
            Object object, Field field, Object value)
            throws IllegalAccessException {

        field.setAccessible(true);

        if (value == null)
            field.set(object, null);
        else {
            Object typedValue = SqlTypeAdapter.toJavaType(
                    field.getType(),
                    value
            );
            field.set(object, typedValue);
        }
    }

    /**
     * According to Entity field associated with the
     * database column, use the database column names
     * to find the corresponding fields.
     *
     * @param entityClass Entity class
     * @param columnName  the name of the database table column
     * @return the field which related with column
     */
    public static Field findField(
            Class entityClass, String columnName) {

        List<Field> fields =
                JormReflect.getMapFields(entityClass);

        for (Field field : fields) {

            JormMap jormMap = field.getAnnotation(JormMap.class);
            boolean columnPresent = jormMap.column().length() > 0;

            String bindColumn = columnPresent ?
                    jormMap.column() : field.getName();

            if (columnName.equalsIgnoreCase(bindColumn)) {
                return field;
            }
        }

        return null;
    }

    /**
     * JORM load an Entity instance of the framework,
     * we need to use parameters to specify the
     * primary key value for this Entity instance.
     * <p/>
     * There is only one primary key for the database
     * tables, primary key value is a single, uniquely
     * determine the value of an Entity instance,
     * which can be an integer, a string, etc.
     * <p/>
     * For there are two or more database tables
     * primary key, JORM framework, agreement, direct
     * the use of an Entity instance as the primary
     * key value, this instance has been assigned
     * primary key values of the corresponding fields.
     * <p/>
     * This method is doing is to determine the primary
     * key value passed in is not an instance of the
     * class Entity:
     * If it is, on the primary key for Entity class
     * list and obtain their values in tabular form
     * to return.
     * If not, directly returns a list containing
     * only the primary key value.
     *
     * @param entityId primary key value
     * @return the list of primary key values 
     */
    public static Object[] idValues(Object entityId) {

        Object[] params;

        if (entityId.getClass().
                isAnnotationPresent(JormEntity.class)) {

            List<Field> primaryKeys =
                    getPkFields(entityId.getClass());

            params = new Object[primaryKeys.size()];

            try {
                for (int i = 0; i < params.length; i++) {
                    Field field = primaryKeys.get(i);
                    field.setAccessible(true);
                    params[i] = field.get(entityId);
                }
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
        else {
            params = new Object[]{entityId};
        }

        return params;
    }
}